﻿using System;
using System.Diagnostics;
using System.Threading;

namespace Clock.Utils
{
    public sealed class SysClock : IDisposable
    {
        private int _isStarted = 0;
        private int _isDisposed = 0;
        private volatile bool _isEnable = false;
        private volatile bool _quitRequested = false;
        private Func<long, uint> _interval = null;
        private object _state = null;
        private bool _isLog = false;
        private int _logRate = 1;
        private Action<string> _logger = null;
        private bool _isUseThreadPool = false;
        private AutoResetEvent _timerEvent = new AutoResetEvent(false);

        public delegate void TickHandler(TickArgs e);

        public event TickHandler Tick;

        public SysClock(Action<Options> options)
        {
            this.Is_Arg_Null(options, nameof(options));

            var opts = new Options();

            options.Invoke(opts);

            this.Is_Arg_Null(opts.IntervalMilliseconds, nameof(opts) + "." + nameof(opts.IntervalMilliseconds));

            this._interval = opts.IntervalMilliseconds;
            this._state = opts.State;
            this._logRate = Math.Max((int)opts.LogRate, 1);
            this._isLog = opts.IsLog;
            this._logger = opts.Logger ?? Console.WriteLine;
            this._isUseThreadPool = opts.IsUseThreadPool;
        }

        public bool IsDisposed => this._isDisposed != 0;

        public bool Enable
        {
            get => this._isEnable;
            set
            {
                this.Check_Throw();

                this._isEnable = value;

                if (value)
                {
                    this.Start();
                }
            }
        }

        public void Dispose()
        {
            if (Interlocked.CompareExchange(ref this._isDisposed, 1, 0) == 0)
            {
                this._quitRequested = true;

                this._timerEvent.Set();
            }
        }

        private void Start()
        {
            if (Interlocked.CompareExchange(ref this._isStarted, 1, 0) == 0)
            {
                if (this._isUseThreadPool)
                {
                    ThreadPool.QueueUserWorkItem(s => this.StartBackgroundloop());
                }
                else
                {
                    var thread = new Thread(this.StartBackgroundloop);

                    thread.Name = string.Concat(typeof(SysClock).FullName, "-", thread.ManagedThreadId.ToString());
                    thread.Priority = ThreadPriority.Normal;
                    thread.IsBackground = true;
                    thread.Start();
                }
            }
            else
            {
                this._timerEvent.Set();
            }
        }

        private void LogEx(Exception e)
        {
            this._logger.Invoke($"{e.Message}{Environment.NewLine}{Environment.NewLine}{e.StackTrace}");
        }

        private void Is_Arg_Null<T>(T t, string argName = null) where T : class
        {
            if ((object)t == null)
            {
                throw new ArgumentNullException(argName, string.Format(@"Argument ""{0}"" can not be null .", argName));
            }
        }

        private void Check_Throw(string name = null)
        {
            if (this.IsDisposed)
            {
                var type = this.GetType();

                throw new ObjectDisposedException(name ?? type.Name, $"{type.AssemblyQualifiedName} was disposed .");
            }
        }

        private void StartBackgroundloop()
        {
            double nextTick = 0;
            long lastInterval = 0;
            long index = 0;
            double frameStart;
            double totalFrameTime = 0;
            double maxFrameTime = 0;
            var timer = Stopwatch.StartNew();

            timer.Start();

            while (true)
            {
                if (!this._isEnable)
                {
                    try
                    {
                        this._timerEvent.WaitOne();
                    }
                    catch (Exception e)
                    {
                        this.LogEx(e);
                    }
                }

                if (this._quitRequested)
                {
                    try
                    {
                        this._timerEvent.Dispose();
                    }
                    catch (Exception e)
                    {
                        this.LogEx(e);
                    }

                    break;
                }

                frameStart = timer.Elapsed.TotalMilliseconds;

                try
                {
                    lastInterval = this._interval.Invoke(index);
                }
                catch (Exception e)
                {
                    this.LogEx(e);
                }

                try
                {
                    this.Tick?.Invoke(new TickArgs(this, index, lastInterval, frameStart, this._state));
                }
                catch (Exception e)
                {
                    this.LogEx(e);
                }

                totalFrameTime += timer.Elapsed.TotalMilliseconds - frameStart;

                maxFrameTime = Math.Max(maxFrameTime, timer.Elapsed.TotalMilliseconds - frameStart);

                nextTick += lastInterval;

                index++;

                if (this._isLog)
                {
                    if (index % this._logRate == 0)
                    {
                        index = 0;
                        this._logger.Invoke($"*-----------------------*\n{timer.Elapsed.TotalMilliseconds:0.000} => Logging performance\n\t{totalFrameTime:0.000} => Cumultative delta times since last log\n\t{maxFrameTime:0.000} => Max delta time since last log\n*-----------------------*");
                        totalFrameTime = 0;
                        maxFrameTime = 0;
                    }
                }

                if (timer.Elapsed.TotalMilliseconds < nextTick)
                {
                    try
                    {
                        this._timerEvent.WaitOne((int)(nextTick - timer.Elapsed.TotalMilliseconds));
                    }
                    catch (Exception e)
                    {
                        this.LogEx(e);
                    }
                }
            }

            timer.Stop();
        }

        public sealed class Options
        {
            public Func<long, uint> IntervalMilliseconds { get; set; }

            public object State { get; set; }

            public bool IsLog { get; set; }

            public uint LogRate { get; set; } = 1;

            public Action<string> Logger { get; set; }

            public bool IsUseThreadPool { get; set; }
        }

        public readonly struct TickArgs
        {
            internal TickArgs(SysClock clock, long index, long lastInterval, double totalMilliseconds, object state)
            {
                this.Clock = clock;
                this.Index = index;
                this.LastInterval = lastInterval;
                this.TotalMilliseconds = totalMilliseconds;
                this.State = state;
                this.HasValue = true;
            }

            public SysClock Clock { get; }

            public long Index { get; }

            public long LastInterval { get; }

            public double TotalMilliseconds { get; }

            public object State { get; }

            public bool HasValue { get; }
        }
    }
}